﻿// License: The MIT License (MIT) Copyright (c) 2011..2012 Barend Gehrels

// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Stormy;

/*
 * UltraID3Lib:
 *   performance: 49 seconds for ~1800 files (release mode)
 *   drawbacks: it has no AlbumArtist!!!!
 *   pictures: not tried.
 * 
 * taglib-sharp: 
 *   performance: 17 seconds for ~1800 files (release mode)
 * 
 * 
 */
#pragma warning disable 618


using System.Diagnostics;
using System.ComponentModel;

namespace GrooveSharp
{
    /// <summary>
    /// Recursively scans folders from a specified base-folder and adds everything not found (albums,artists,tracks,genres) to the SQLite DB
    /// </summary>
    class FolderScanner
    {
        private class TrackWrapper
        {
            public Track WrappedTrack { get; set; }
            public bool Found { get; set; }
        }

        private IDictionary<string, Album> m_albums;
        private IDictionary<string, Artist> m_artists;
        private IDictionary<string, Genre> m_genres;
        private IList<TrackWrapper> m_tracks;

        private delegate int DelegateGetId<T>(T input);
        private delegate T DelegateGetNew<T>(string name);

        public FolderScanner()
        {
            FillDictionaries(Db.Connection());
        }

        // Generic method to add an album, Artist or Genre
        private static int GetOrInsert<T>(IDictionary<string, T> dictionary, string key, 
                    DelegateGetId<T> getId, 
                    DelegateGetNew<T> getNewValue)
        {
            if (key == null)
            {
                key = "";
            }

            if (!dictionary.ContainsKey(key))
            {
                T item = getNewValue(key);
                dictionary.Add(key, item);
                Db.Connection().Insert(item);
                return getId(item);
            }
            else
            {
                return getId(dictionary[key]);
            }
        }

        public static void Split(string baseFolderName, string path, out string folderName, out string fileName)
        {
            fileName = Path.GetFileName(path);
            folderName = Path.GetDirectoryName(path).ToLowerInvariant().Replace(baseFolderName.ToLowerInvariant(), "");
            // Strip starting \, if any
            if (folderName.StartsWith(@"\"))
            {
                folderName = folderName.Substring(1);
            }
        }

        private bool CheckIfTrackIsIncluded(Track track)
        {
            // Tracks can be null if called from watcher.
            if (m_tracks == null)
            {
                return false;
            }

            bool result = false;
            foreach (var item in m_tracks)
            {
                if (item.WrappedTrack.PathId == track.PathId
                    && item.WrappedTrack.FileSize == track.FileSize
                    && string.Equals(item.WrappedTrack.FolderName, track.FolderName)
                    && string.Equals(item.WrappedTrack.FileName, track.FileName))
                {
                    result = true;
                    item.Found = true;
                }
            }
            return result;
        }
        
        private void AddTrack(GroovePath path, string baseFolderName, string fullPath)
        {
            try
            {
                Track track = new Track();
                track.FileSize = new FileInfo(fullPath).Length;
                track.PathId = path.Id;

                string fileName, folderName;
                Split(baseFolderName, fullPath, out folderName, out fileName);

                track.FileName = fileName;
                track.FolderName = folderName;

                if (CheckIfTrackIsIncluded(track))
                {
                    return;
                }


                using (TagLib.File tagFile = TagLib.File.Create(fullPath))
                {
                    track.Title = TrackInfoHelper.GetTitle(tagFile, fullPath);
                    track.Year = (int)tagFile.Tag.Year;
                    track.DiscNumber = (int)(tagFile.Tag.Disc > 0 ? tagFile.Tag.Disc : 1);
                    track.TrackNumber = (int)tagFile.Tag.Track;
                    track.TrackCount = (int)tagFile.Tag.TrackCount;
                    track.Duration = tagFile.Properties.Duration;

                    string artist = TrackInfoHelper.GetArtist(tagFile, fullPath, true);

                    track.AlbumId = GetOrInsert(m_albums, TrackInfoHelper.GetAlbum(tagFile, fullPath),
                                        (entry) => entry.Id,
                                        (nm) => new Album() { Title = nm });
                    track.ArtistId = GetOrInsert(m_artists, artist.GetSortName(),
                                        (entry) => entry.Id,
                                        (nm) => new Artist() { Name = artist, SortName = nm });
                    track.GenreId = GetOrInsert(m_genres, tagFile.Tag.FirstGenre,
                                        (entry) => entry.Id,
                                        (nm) => new Genre() { Name = nm });
                }

                Db.Connection().Insert(track);
                App.Log.Info("Inserted: " + track.FileName);
            }
            catch (Exception ex)
            {
                App.Log.ErrorFormat("Exception in adding MP3 file {0}: {1}", fullPath, ex.Message);
            }
        }

        private void AddToDb(GroovePath path, string baseFolderName, string folderName, int count, ref int index, BackgroundWorker worker)
        {
            try
            {
                var subFolders = Directory.GetDirectories(folderName);

                foreach (string subFolder in subFolders)
                {
                    AddToDb(path, baseFolderName, subFolder, count, ref index, null);

                    if (worker != null && worker.WorkerReportsProgress)
                    {
                        index++;
                        int percentage = 100 * index / count;
                        worker.ReportProgress(percentage, "Scanning " + subFolder);
                        App.Log.InfoFormat("SCAN {0} of {1} -> {2}%", index, count, percentage);
                    }
                }

                foreach (string file in Directory.GetFiles(folderName, "*.mp3"))
                {
                    AddTrack(path, baseFolderName, file);
                }
                foreach (string file in Directory.GetFiles(folderName, "*.mp4"))
                {
                    AddTrack(path, baseFolderName, file);
                }
                foreach (string file in Directory.GetFiles(folderName, "*.m4a"))
                {
                    AddTrack(path, baseFolderName, file);
                }
            }
            catch (Exception ex)
            {
                App.Log.ErrorFormat("Exception in adding folder {0}: {1}", path.PathName, ex.Message);
            }
        }

        private void FillDictionaries(Connection db)
        {
            m_albums = db.Select<Album>("select * from albums").ToDictionary(item => item.Title, item => item);
            m_artists = db.Select<Artist>("select * from artists").ToDictionary(item => item.SortName, item => item);
            m_genres = db.Select<Genre>("select * from genres").ToDictionary(item => item.Name, item => item);
        }

        private void DbUpdateTrackIsDeletedFlag(bool newFlag, IEnumerable<int> ids)
        {
            if (ids.Count() > 0)
            {
                string sql = String.Format("update tracks set is_deleted={0} where track_id in ({1})", (newFlag ? 1 : 0), string.Join(",", ids));
                Db.Connection().Execute(sql);
                App.Log.InfoFormat("Marked for deletion: tracks {0}", ids);
            }
        }

        public string PathWithoutBackSlash(string path)
        {
            // Strip final \, if any
            return path.EndsWith(@"\") 
                ? path.Substring(0, path.Length - 1)
                : path;
        }

        public void Scan(GroovePath path, int count, ref int index, BackgroundWorker worker)
        {
            // Get all tracks (order them to find them faster (NYI))
            m_tracks =
                (
                    from track
                    in Db.Connection().Select<Track>(String.Format("select * from tracks where path_id={0} order by file_size", path.Id))
                    select new TrackWrapper() { WrappedTrack = track }
                ).ToList();

            AddToDb(path, PathWithoutBackSlash(path.PathName), path.PathName, count, ref index, worker);

            DbUpdateTrackIsDeletedFlag(true,  from track in m_tracks where ! track.Found && ! track.WrappedTrack.IsDeleted select track.WrappedTrack.Id);
            DbUpdateTrackIsDeletedFlag(false, from track in m_tracks where   track.Found &&   track.WrappedTrack.IsDeleted select track.WrappedTrack.Id);
        }

        public void AddFile(GroovePath path, string fileName)
        {
            path.PathName = Path.GetFullPath(path.PathName);

            m_tracks = null;
            AddTrack(path, PathWithoutBackSlash(path.PathName), fileName);
        }

    }
}
